package com.tom.service.impl;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.baomidou.mybatisplus.mapper.Wrapper;
import com.baomidou.mybatisplus.service.impl.ServiceImpl;
import com.tom.config.RabbitMQConfig;
import com.tom.domain.KillCourse;
import com.tom.dto.KillParamDto;
import com.tom.exception.GloblException;
import com.tom.mapper.KillCourseMapper;
import com.tom.service.IKillCourseService;
import com.tom.to.PrepareOrderTo;
import com.tom.util.AssertUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateUtils;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.security.auth.login.LoginContext;
import java.util.Date;
import java.util.List;
import java.util.UUID;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @since 2021-12-09
 */
@Service
@Slf4j
public class KillCourseServiceImpl extends ServiceImpl<KillCourseMapper, KillCourse> implements IKillCourseService {

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate ;

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private RabbitTemplate rabbitTemplate ;

    @Override
    public void add(KillCourse killCourse) {
        //参数判断 省略...
        killCourse.setSessionNumber(UUID.randomUUID().toString());
        killCourse.setKillSort(0);
        killCourse.setCreateTime(new Date());
        killCourse.setKillStatus(KillCourse.STATUS_CREATE);
        super.insert(killCourse);
    }

    /**
     * 把近2天即将秒杀的课程发布到Redis
     * 课程使用hash存储
     * 1.查询出2天即将秒杀的课程
     * 2.循环课程列表
     * 2.1.以hash存储课程到Redis
     * 2.2.把库存以信号量存储到Redis
     * 2.3.把课程的状态修改为已经发布（DB）
     * 2.4.为每个秒杀课程生成一个随机码
     */
    @Override
    public void publishKillCourse2Redis() {
        log.info("定时发布课程");
        //* 1.查询出2天即将秒杀的课程
        Date now = new Date();
        Date endDate = DateUtils.addDays(now, 2);
        Wrapper<KillCourse> wrapper = new EntityWrapper<>();
        wrapper.between("start_time", now, endDate);
        wrapper.eq("kill_status", KillCourse.STATUS_CREATE);
        List<KillCourse> killCourses = super.selectList(wrapper);
        //* 2.循环课程列表
        killCourses.forEach(killCourse -> {
            //* 2.4.为每个秒杀课程生成一个随机码
            String code = UUID.randomUUID().toString();
            killCourse.setCode(code);
            //* 2.1.以hash存储课程到Redis , 防止重复
            Boolean success1 = redisTemplate.opsForHash().putIfAbsent("KILL_COURSES", killCourse.getId().toString(), killCourse);
            log.info("把课程发布到redis {} - {}",success1,killCourse);
            if(success1){
                //* 2.2.把库存以信号量存储到Redis
                RSemaphore semaphore = redissonClient.getSemaphore("STORE_" + killCourse.getId());
                boolean success2 = semaphore.trySetPermits(killCourse.getKillCount());
                log.info("把库存发布到redis: {} - {}",success2,killCourse.getKillCount());
                if(success2){
                    //* 2.3.把课程的状态修改为已经发布（DB）
                    killCourse.setKillStatus(KillCourse.STATUS_PUBLISH);
                    super.updateById(killCourse);
                    log.info("修改数据库课程状态 {}",killCourse);
                }else{
                    //删除课程
                    redisTemplate.opsForHash().delete("KILL_COURSES",killCourse.getId().toString());
                    log.error("库存存储到Redis失败 {}",success2);
                }
            }
        });
    }

    @Override
    public List<?> selectPublishedFromCache() {
        /**
        BoundHashOperations<Object, Object, KillCourse> kill_courses = redisTemplate.boundHashOps("KILL_COURSES");
        List<KillCourse> values = kill_courses.values(); **/

        return redisTemplate.opsForHash().values("KILL_COURSES");
    }

    @Override
    public KillCourse selectPublishedOne(Long id) {
        AssertUtil.isNotNull(id,"非法请求" );
        return (KillCourse) redisTemplate.opsForHash().get("KILL_COURSES", id.toString());
    }

    /**
     1.拿到秒杀课程ID
     2.判断用户是否登录
     3.用户是否已经秒杀过该课程了
     4.判断秒杀课程的开始时间和结束时间是否合法
     5.判断秒杀课程的随机码是否正确
     6.尝试使用信号量减去库存 ：-1
     7.创建一个订单号，构建预创订单数据
     8.把订单存储到Redis中
     9.保存秒杀成功记录到Redis
     * @param dto ： 课程ID ，随机码
     * @return : 返回订单号
     */
    @Override
    public void kill(KillParamDto dto) {
        Long startTime = System.currentTimeMillis();
        //0.判断参数
        String code = dto.getCode();
        //1.拿到秒杀课程ID
        Long killCourseId = dto.getId();
        AssertUtil.isNotEmpty(code,"非法请求" );
        AssertUtil.isNotNull(killCourseId,"非法请求" );
        //2.判断用户是否登录
        //TODO 模拟登陆用户
        Long userId = 3L;
        //3.用户是否已经秒杀过该课程了
        String orderKey = "ORDER_"+userId+"_"+killCourseId;
        Boolean canKill = redisTemplate.hasKey(orderKey);
        //AssertUtil.isFalse(canKill,"不能重复秒杀" );

        //4.判断秒杀课程的开始时间和结束时间是否合法
        KillCourse killCourse = (KillCourse) redisTemplate.opsForHash().get("KILL_COURSES", killCourseId.toString());
        AssertUtil.isNotNull(killCourse,"课程不存在" );
        Date now = new Date();
        if(now.before(killCourse.getStartTime()) || now.after( killCourse.getEndTime())){
            throw new GloblException("秒杀时间不合法");
        }
        //5.判断秒杀课程的随机码是否正确
        AssertUtil.isEquals(killCourse.getCode() , code, "非法请求");

        //6.尝试使用信号量减去库存 ：-1
        RSemaphore semaphore = redissonClient.getSemaphore("STORE_" + killCourse.getId());
        //减去1个库存
        boolean killSuccess = semaphore.tryAcquire(1);
        AssertUtil.isTrue(killSuccess, "手速太慢，活该你不单身");

        //7.创建一个订单号，构建预创订单数据
        String orderNo = UUID.randomUUID().toString();
        PrepareOrderTo prepareOrderTo = new PrepareOrderTo(killCourseId, killCourse.getCourseName(), userId, orderNo, now, killCourse.getKillPrice());

        //8.把订单存储到Redis中
        redisTemplate.opsForValue().set(orderKey,prepareOrderTo );

        //9.保存延迟消息
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_TOPIC_KILL
                , "order.delay", orderKey);

        log.info("秒杀耗时 {}" ,System.currentTimeMillis() - startTime );
    }
}
